<?php

/**
 * Class PerspectiveTransform
 *
 * @created      17.01.2021
 *
 * @author       ZXing Authors
 * @author       Smiley <smiley@chillerlan.net>
 * @copyright    2021 Smiley
 * @license      Apache-2.0
 */
declare(strict_types=1);

namespace zxf\QrCode\Detector;

use function count;

/**
 * This class implements a perspective transform in two dimensions. Given four source and four
 * destination points, it will compute the transformation implied between them. The code is based
 * directly upon section 3.4.2 of George Wolberg's "Digital Image Warping"; see pages 54-56.
 *
 * @author Sean Owen
 */
final class PerspectiveTransform
{
    private float $a11;

    private float $a12;

    private float $a13;

    private float $a21;

    private float $a22;

    private float $a23;

    private float $a31;

    private float $a32;

    private float $a33;

    private function set(
        float $a11, float $a21, float $a31,
        float $a12, float $a22, float $a32,
        float $a13, float $a23, float $a33,
    ): self {
        $this->a11 = $a11;
        $this->a12 = $a12;
        $this->a13 = $a13;
        $this->a21 = $a21;
        $this->a22 = $a22;
        $this->a23 = $a23;
        $this->a31 = $a31;
        $this->a32 = $a32;
        $this->a33 = $a33;

        return $this;
    }

    public function quadrilateralToQuadrilateral(
        float $x0, float $y0, float $x1, float $y1, float $x2, float $y2, float $x3, float $y3,
        float $x0p, float $y0p, float $x1p, float $y1p, float $x2p, float $y2p, float $x3p, float $y3p,
    ): self {
        return (new self)
            ->squareToQuadrilateral($x0p, $y0p, $x1p, $y1p, $x2p, $y2p, $x3p, $y3p)
            ->times($this->quadrilateralToSquare($x0, $y0, $x1, $y1, $x2, $y2, $x3, $y3));
    }

    private function quadrilateralToSquare(
        float $x0, float $y0, float $x1, float $y1,
        float $x2, float $y2, float $x3, float $y3,
    ): self {
        // Here, the adjoint serves as the inverse:
        return $this
            ->squareToQuadrilateral($x0, $y0, $x1, $y1, $x2, $y2, $x3, $y3)
            ->buildAdjoint();
    }

    private function buildAdjoint(): self
    {
        // Adjoint is the transpose of the cofactor matrix:
        return $this->set(
            ($this->a22 * $this->a33 - $this->a23 * $this->a32),
            ($this->a23 * $this->a31 - $this->a21 * $this->a33),
            ($this->a21 * $this->a32 - $this->a22 * $this->a31),
            ($this->a13 * $this->a32 - $this->a12 * $this->a33),
            ($this->a11 * $this->a33 - $this->a13 * $this->a31),
            ($this->a12 * $this->a31 - $this->a11 * $this->a32),
            ($this->a12 * $this->a23 - $this->a13 * $this->a22),
            ($this->a13 * $this->a21 - $this->a11 * $this->a23),
            ($this->a11 * $this->a22 - $this->a12 * $this->a21),
        );
    }

    private function squareToQuadrilateral(
        float $x0, float $y0, float $x1, float $y1,
        float $x2, float $y2, float $x3, float $y3,
    ): self {
        $dx3 = ($x0 - $x1 + $x2 - $x3);
        $dy3 = ($y0 - $y1 + $y2 - $y3);

        if ($dx3 === 0.0 && $dy3 === 0.0) {
            // Affine
            return $this->set(($x1 - $x0), ($x2 - $x1), $x0, ($y1 - $y0), ($y2 - $y1), $y0, 0.0, 0.0, 1.0);
        }

        $dx1 = ($x1 - $x2);
        $dx2 = ($x3 - $x2);
        $dy1 = ($y1 - $y2);
        $dy2 = ($y3 - $y2);
        $denominator = ($dx1 * $dy2 - $dx2 * $dy1);
        $a13 = (($dx3 * $dy2 - $dx2 * $dy3) / $denominator);
        $a23 = (($dx1 * $dy3 - $dx3 * $dy1) / $denominator);

        return $this->set(
            ($x1 - $x0 + $a13 * $x1),
            ($x3 - $x0 + $a23 * $x3),
            $x0,
            ($y1 - $y0 + $a13 * $y1),
            ($y3 - $y0 + $a23 * $y3),
            $y0,
            $a13,
            $a23,
            1.0,
        );
    }

    private function times(PerspectiveTransform $other): self
    {
        return $this->set(
            ($this->a11 * $other->a11 + $this->a21 * $other->a12 + $this->a31 * $other->a13),
            ($this->a11 * $other->a21 + $this->a21 * $other->a22 + $this->a31 * $other->a23),
            ($this->a11 * $other->a31 + $this->a21 * $other->a32 + $this->a31 * $other->a33),
            ($this->a12 * $other->a11 + $this->a22 * $other->a12 + $this->a32 * $other->a13),
            ($this->a12 * $other->a21 + $this->a22 * $other->a22 + $this->a32 * $other->a23),
            ($this->a12 * $other->a31 + $this->a22 * $other->a32 + $this->a32 * $other->a33),
            ($this->a13 * $other->a11 + $this->a23 * $other->a12 + $this->a33 * $other->a13),
            ($this->a13 * $other->a21 + $this->a23 * $other->a22 + $this->a33 * $other->a23),
            ($this->a13 * $other->a31 + $this->a23 * $other->a32 + $this->a33 * $other->a33),
        );
    }

    /**
     * @param  float[]  $xValues
     * @param  float[]|null  $yValues
     * @return float[][] [$xValues, $yValues]
     */
    public function transformPoints(array $xValues, ?array $yValues = null): array
    {
        $max = count($xValues);

        if ($yValues !== null) { // unused

            for ($i = 0; $i < $max; $i++) {
                $x = $xValues[$i];
                $y = $yValues[$i];
                $denominator = ($this->a13 * $x + $this->a23 * $y + $this->a33);
                $xValues[$i] = (($this->a11 * $x + $this->a21 * $y + $this->a31) / $denominator);
                $yValues[$i] = (($this->a12 * $x + $this->a22 * $y + $this->a32) / $denominator);
            }

            return [$xValues, $yValues];
        }

        for ($i = 0; $i < $max; $i += 2) {
            $x = $xValues[$i];
            $y = $xValues[($i + 1)];
            $denominator = ($this->a13 * $x + $this->a23 * $y + $this->a33);
            $xValues[$i] = (($this->a11 * $x + $this->a21 * $y + $this->a31) / $denominator);
            $xValues[($i + 1)] = (($this->a12 * $x + $this->a22 * $y + $this->a32) / $denominator);
        }

        return [$xValues, []];
    }
}
